usbx 的主从 OTG
usbx 基本介绍
usbx 是 threadx 操作系统的 USB 设备栈,支持多种主机驱动、从机枚举、以及 OTG
开源库地址在这里
介绍在这里
如果使用 threadx RTOS,使用这个 USB 设备栈单独作为主机或从机的实现是非常简单的,但是如果希望同时使用这个库的主从功能(OTG)则会稍显困难,因为官方文档只有只言片语,各个论坛也没有一个完整细致的例子。
usbx 的 OTG 并非指硬件 OTG ,而是协议层的 OTG ,意味着可以同时使用 usbx 协议栈的主从功能,使得 host 对象和 device 对象能同时正常驱动,而硬件层是一个还是多个接口,usbx 是不会在意的。
首先确认自己具备以下的能力
- 对 C语言库 如何使用宏配置,控制一些代码的行为,有基础的了解。
- 具备看懂官方例程的能力,指能看懂官方例程 main 主流程的每一句在干什么。
- 手动或借助工具配置芯片外设的能力,后续不涉及外设驱动部分。
使用库的基本姿势
以下是开启 host 协议栈的基本姿势,总的来说,就是定义一片内存区域作为内存池,然后从这个内存池取一片内存给 usbx 系统使用,将需要使用的主机类注册一下,再取一片内存开启主机应用线程。主机应用线程里需要做什么可以参考官方例程。
#define UX_HOST_APP_MEM_POOL_SIZE 1024 * 44
__ALIGN_BEGIN static UCHAR ux_host_byte_pool_buffer[UX_HOST_APP_MEM_POOL_SIZE] __ALIGN_END;
static TX_BYTE_POOL ux_host_app_byte_pool;
void txAppUSBXHostInit(void)
{
UINT status = TX_SUCCESS;
VOID *memory_ptr;
if(tx_byte_pool_create(&ux_host_app_byte_pool, "Ux App memory pool", ux_host_byte_pool_buffer, UX_HOST_APP_MEM_POOL_SIZE) != TX_SUCCESS)
{
}
else
{
memory_ptr = (VOID *)&ux_host_app_byte_pool;
status = MX_USBX_Host_Init(memory_ptr);
if(status != UX_SUCCESS)
{
while(1)
{
}
}
}
}
UINT MX_USBX_Host_Init(VOID *memory_ptr)
{
UINT ret = UX_SUCCESS;
UCHAR *pointer;
TX_BYTE_POOL *byte_pool = (TX_BYTE_POOL *)memory_ptr;
if(tx_byte_allocate(byte_pool, (VOID **)&pointer, USBX_HOST_MEMORY_STACK_SIZE, TX_NO_WAIT) != TX_SUCCESS)
{
return TX_POOL_ERROR;
}
if(ux_system_initialize(pointer, USBX_HOST_MEMORY_STACK_SIZE, UX_NULL, 0) != UX_SUCCESS)
{
return UX_ERROR;
}
if(ux_host_stack_initialize(ux_host_event_callback) != UX_SUCCESS)
{
return UX_ERROR;
}
ux_utility_error_callback_register(&ux_host_error_callback);
if(ux_host_stack_class_register(_ux_system_host_class_storage_name, ux_host_class_storage_entry) != UX_SUCCESS)
{
return UX_ERROR;
}
if(tx_byte_allocate(byte_pool, (VOID **)&pointer, UX_HOST_APP_THREAD_STACK_SIZE, TX_NO_WAIT) != TX_SUCCESS)
{
return TX_POOL_ERROR;
}
if(tx_thread_create(&ux_host_app_thread, UX_HOST_APP_THREAD_NAME, app_ux_host_thread_entry, 0, pointer, UX_HOST_APP_THREAD_STACK_SIZE, UX_HOST_APP_THREAD_PRIO, UX_HOST_APP_THREAD_PREEMPTION_THRESHOLD, UX_HOST_APP_THREAD_TIME_SLICE, UX_HOST_APP_THREAD_START_OPTION) != TX_SUCCESS)
{
return TX_THREAD_ERROR;
}
return ret;
}
以下是开启 drvice 协议栈的基本姿势,总的来说,和主机操作类似,建立内存池,取内存,初始化和注册,开启从机应用线程。
#define UX_DEVICE_APP_MEM_POOL_SIZE 1024 * 24
__ALIGN_BEGIN static UCHAR ux_device_byte_pool_buffer[UX_DEVICE_APP_MEM_POOL_SIZE] __ALIGN_END;
static TX_BYTE_POOL ux_device_app_byte_pool;
void txAppUSBXDrviceInit(void)
{
UINT status = TX_SUCCESS;
VOID *memory_ptr;
if(tx_byte_pool_create(&ux_device_app_byte_pool, "Ux App memory pool", ux_device_byte_pool_buffer, UX_DEVICE_APP_MEM_POOL_SIZE) != TX_SUCCESS)
{
}
else
{
memory_ptr = (VOID *)&ux_device_app_byte_pool;
status = MX_USBX_Device_Init(memory_ptr);
if(status != UX_SUCCESS)
{
while(1)
{
}
}
}
}
UINT MX_USBX_Device_Init(VOID *memory_ptr)
{
UINT ret = UX_SUCCESS;
UCHAR *device_framework_high_speed;
UCHAR *device_framework_full_speed;
ULONG device_framework_hs_length;
ULONG device_framework_fs_length;
ULONG string_framework_length;
ULONG language_id_framework_length;
UCHAR *string_framework;
UCHAR *language_id_framework;
UCHAR *pointer;
TX_BYTE_POOL *byte_pool = (TX_BYTE_POOL *)memory_ptr;
if(tx_byte_allocate(byte_pool, (VOID **)&pointer, USBX_DEVICE_MEMORY_STACK_SIZE, TX_NO_WAIT) != TX_SUCCESS)
{
return TX_POOL_ERROR;
}
if(ux_system_initialize(pointer, USBX_DEVICE_MEMORY_STACK_SIZE, UX_NULL, 0) != UX_SUCCESS)
{
return UX_ERROR;
}
device_framework_high_speed = USBD_Get_Device_Framework_Speed(USBD_HIGH_SPEED, &device_framework_hs_length);
device_framework_full_speed = USBD_Get_Device_Framework_Speed(USBD_FULL_SPEED, &device_framework_fs_length);
string_framework = USBD_Get_String_Framework(&string_framework_length);
language_id_framework = USBD_Get_Language_Id_Framework(&language_id_framework_length);
if(ux_device_stack_initialize(device_framework_high_speed, device_framework_hs_length, device_framework_full_speed, device_framework_fs_length, string_framework, string_framework_length, language_id_framework, language_id_framework_length, UX_NULL) != UX_SUCCESS)
{
return UX_ERROR;
}
cdc_acm_parameter.ux_slave_class_cdc_acm_instance_activate = USBD_CDC_ACM_Activate;
cdc_acm_parameter.ux_slave_class_cdc_acm_instance_deactivate = USBD_CDC_ACM_Deactivate;
cdc_acm_parameter.ux_slave_class_cdc_acm_parameter_change = USBD_CDC_ACM_ParameterChange;
cdc_acm_configuration_number = USBD_Get_Configuration_Number(CLASS_TYPE_CDC_ACM, 0);
cdc_acm_interface_number = USBD_Get_Interface_Number(CLASS_TYPE_CDC_ACM, 0);
if(ux_device_stack_class_register(_ux_system_slave_class_cdc_acm_name, ux_device_class_cdc_acm_entry, cdc_acm_configuration_number, cdc_acm_interface_number, &cdc_acm_parameter) != UX_SUCCESS)
{
return UX_ERROR;
}
if(tx_byte_allocate(byte_pool, (VOID **)&pointer, UX_DEVICE_APP_THREAD_STACK_SIZE, TX_NO_WAIT) != TX_SUCCESS)
{
return TX_POOL_ERROR;
}
if(tx_thread_create(&ux_device_app_thread, UX_DEVICE_APP_THREAD_NAME, app_ux_device_thread_entry, 0, pointer, UX_DEVICE_APP_THREAD_STACK_SIZE, UX_DEVICE_APP_THREAD_PRIO, UX_DEVICE_APP_THREAD_PREEMPTION_THRESHOLD, UX_DEVICE_APP_THREAD_TIME_SLICE, UX_DEVICE_APP_THREAD_START_OPTION) != TX_SUCCESS)
{
return TX_THREAD_ERROR;
}
if(tx_byte_allocate(byte_pool, (VOID **)&pointer, 1024, TX_NO_WAIT) != TX_SUCCESS)
{
return TX_POOL_ERROR;
}
if(tx_thread_create(&ux_cdc_read_thread, "cdc_acm_read_usbx_app_thread_entry", usbx_cdc_acm_read_thread_entry, 1, pointer, 1024, 20, 20, TX_NO_TIME_SLICE, TX_AUTO_START) != TX_SUCCESS)
{
return TX_THREAD_ERROR;
}
return ret;
}
关于开启 usbx OTG 的特殊操作
首先使用 usbx,添加编译全局 define UX_INCLUDE_USER_DEFINE_FILE
,是一个好的操作。可以使用 ux_user.h 控制裁剪 usbx 的各个功能。库源码中应该有 ux_user_sample.h 这样命名的文件,复制一份并重命名为 ux_user.h 加入工程。
其中应该有类似如下的部分,这是带来疑惑的主要内容,原版示例中将 UX_OTG_SUPPORT
的定义部分注释掉了,在自己的 ux_user.h 中需要将 UX_OTG_SUPPORT 调整为不要注释,同时注释掉 ux_user.h 中 deine UX_HOST_SIDE_ONLY 和 UX_DEVICE_SIDE_ONLY 的部分。这样库的 OTG 支持就开启了。另外有 UX_DEVICE_BIDIRECTIONAL_ENDPOINT_SUPPORT 和 UX_DEVICE_CLASS_CDC_ACM_WRITE_AUTO_ZLP 宏也建议开启。
#ifndef UX_HOST_SIDE_ONLY
#ifndef UX_DEVICE_SIDE_ONLY
/* #define UX_OTG_SUPPORT */
#endif
#endif
注意到基本姿势中,主机与从机的 init 流程中都调用了 ux_system_initialize
这个函数,这也是直接 copy 例程或者使用 cubemx 直接生成包括主机和从机的工程都不能使用的症结。ux_system_initialize
不可以调用两次,会导致其中一侧的功能异常,且会死在一个内部的处理线程中。正确的姿势是预定义三个内存空间,使用三个内存池,第一个优先取一片内存进行 ux_system_initialize
,第二第三个内存池再进行主机与从机的 init,同时流程中不要再进行 ux_system_initialize
。后续就可以正常使用
另外的细则
windows 的 CDC_ACM 免驱串口固定使用端点 0x01 和 0x81 进行 IN/OUT,这部分禁止修改端点号。
部分报文要求长度必须 4 字节对齐,以下有一个优秀的补零方法。
uint8_t *data; // 原有报文buffer
uint32_t len = OriginalLen; // 原有长度
for(uint32_t i = len; i < ((len + 0x3) & ~0x3U); i++)
{
data[i] = 0x00;
}
_write(data, ((len + 0x3) & ~0x3U));
对于组合枚举,描述符最好在 cubemx 生成的 ux_device_descriptors.c 基础上修改,其实这个生成描述符的源码真的非常优秀了。
需要注意的是,当使用端点时,对应的 USB Txfifo 应当正确开启,有以下示例。
/* Set Rx FIFO */
HAL_PCDEx_SetRxFiFo(&hpcd_USB_OTG_FS, 0x200);
/* Set Tx FIFO 0 */
HAL_PCDEx_SetTxFiFo(&hpcd_USB_OTG_FS, 0, 0x80); // 通用的 setup 端点
/* Set Tx FIFO 1 */
HAL_PCDEx_SetTxFiFo(&hpcd_USB_OTG_FS, 1, 0x100); // 开启了 0x01 和 0x81 端点
/* Set Tx FIFO 3 */
HAL_PCDEx_SetTxFiFo(&hpcd_USB_OTG_FS, 3, 0x100); // 开启了 0x03 和 0x83 端点